/**
 * Copyright (c)2005-2009 Matt Kruse (javascripttoolbox.com)
 * 
 * Dual licensed under the MIT and GPL licenses. 
 * This basically means you can use this code however you want for
 * free, but don't claim to have written it yourself!
 * Donations always accepted: http://www.JavascriptToolbox.com/donate/
 * 
 * Please do not link to the .js files on javascripttoolbox.com from
 * your site. Copy the files locally to your server instead.
 * 
 */
/**
 * Table.js
 * Functions for interactive Tables
 *
 * Copyright (c) 2007 Matt Kruse (javascripttoolbox.com)
 * Dual licensed under the MIT and GPL licenses. 
 *
 * @version 0.981
 *
 * @history 0.981 2007-03-19 Added Sort.numeric_comma, additional date parsing formats
 * @history 0.980 2007-03-18 Release new BETA release pending some testing. Todo: Additional docs, examples, plus jQuery plugin.
 * @history 0.959 2007-03-05 Added more "auto" functionality, couple bug fixes
 * @history 0.958 2007-02-28 Added auto functionality based on class names
 * @history 0.957 2007-02-21 Speed increases, more code cleanup, added Auto Sort functionality
 * @history 0.956 2007-02-16 Cleaned up the code and added Auto Filter functionality.
 * @history 0.950 2006-11-15 First BETA release.
 *
 * @todo Add more date format parsers
 * @todo Add style classes to colgroup tags after sorting/filtering in case the user wants to highlight the whole column
 * @todo Correct for colspans in data rows (this may slow it down)
 * @todo Fix for IE losing form control values after sort?
 */

/**
 * Sort Functions
 */
var Sort = (function() {
	var sort = {};
	// Default alpha-numeric sort
	// --------------------------
	sort.alphanumeric = function(a, b) {
		return (a == b) ? 0 : (a < b) ? -1 : 1;
	};
	sort['default'] = sort.alphanumeric; // IE chokes on sort.default

	// This conversion is generalized to work for either a decimal separator of
	// , or .
	sort.numeric_converter = function(separator) {
		return function(val) {
			if (typeof (val) == "string") {
				val = parseFloat(val.replace(/^[^\d\.]*([\d., ]+).*/g, "$1")
						.replace(new RegExp("[^\\\d" + separator + "]", "g"),
								'').replace(/,/, '.')) || 0;
			}
			return val || 0;
		};
	};

	// Numeric Sort
	// ------------
	sort.numeric = function(a, b) {
		return sort.numeric.convert(a) - sort.numeric.convert(b);
	};
	sort.numeric.convert = sort.numeric_converter(".");

	// Numeric Sort - comma decimal separator
	// --------------------------------------
	sort.numeric_comma = function(a, b) {
		return sort.numeric_comma.convert(a) - sort.numeric_comma.convert(b);
	};
	sort.numeric_comma.convert = sort.numeric_converter(",");

	// Case-insensitive Sort
	// ---------------------
	sort.ignorecase = function(a, b) {
		return sort.alphanumeric(sort.ignorecase.convert(a), sort.ignorecase
				.convert(b));
	};
	sort.ignorecase.convert = function(val) {
		if (val == null) {
			return "";
		}
		return ("" + val).toLowerCase();
	};

	// Currency Sort
	// -------------
	sort.currency = sort.numeric; // Just treat it as numeric!
	sort.currency_comma = sort.numeric_comma;

	// Date sort
	// ---------
	sort.date = function(a, b) {
		return sort.numeric(sort.date.convert(a), sort.date.convert(b));
	};
	// Convert 2-digit years to 4
	sort.date.fixYear = function(yr) {
		yr = +yr;
		if (yr < 50) {
			yr += 2000;
		} else if (yr < 100) {
			yr += 1900;
		}
		return yr;
	};
	sort.date.formats = [
	// YY[YY]-MM-DD
	{
		re : /(\d{2,4})-(\d{1,2})-(\d{1,2})/,
		f : function(x) {
			return (new Date(sort.date.fixYear(x[1]), +x[2], +x[3])).getTime();
		}
	}
	// MM/DD/YY[YY] or MM-DD-YY[YY]
	, {
		re : /(\d{1,2})[\/-](\d{1,2})[\/-](\d{2,4})/,
		f : function(x) {
			return (new Date(sort.date.fixYear(x[3]), +x[1], +x[2])).getTime();
		}
	}
	// Any catch-all format that new Date() can handle. This is not reliable
	// except for long formats, for example: 31 Jan 2000 01:23:45 GMT
	, {
		re : /(.*\d{4}.*\d+:\d+\d+.*)/,
		f : function(x) {
			var d = new Date(x[1]);
			if (d) {
				return d.getTime();
			}
		}
	} ];
	sort.date.convert = function(val) {
		var m, v, f = sort.date.formats;
		for ( var i = 0, L = f.length; i < L; i++) {
			if (m = val.match(f[i].re)) {
				v = f[i].f(m);
				if (typeof (v) != "undefined") {
					return v;
				}
			}
		}
		return 9999999999999; // So non-parsed dates will be last, not first
	};

	return sort;
})();

/**
 * The main Table namespace
 */
var Table = (function() {

	/**
	 * Determine if a reference is defined
	 */
	function def(o) {
		return (typeof o != "undefined");
	}
	;

	/**
	 * Determine if an object or class string contains a given class.
	 */
	function hasClass(o, name) {
		return new RegExp("(^|\\s)" + name + "(\\s|$)").test(o.className);
	}
	;

	/**
	 * Add a class to an object
	 */
	function addClass(o, name) {
		var c = o.className || "";
		if (def(c) && !hasClass(o, name)) {
			o.className += (c ? " " : "") + name;
		}
	}
	;

	/**
	 * Remove a class from an object
	 */
	function removeClass(o, name) {
		var c = o.className || "";
		o.className = c.replace(new RegExp("(^|\\s)" + name + "(\\s|$)"), "$1");
	}
	;

	/**
	 * For classes that match a given substring, return the rest
	 */
	function classValue(o, prefix) {
		var c = o.className;
		if (c.match(new RegExp("(^|\\s)" + prefix + "([^ ]+)"))) {
			return RegExp.$2;
		}
		return null;
	}
	;

	/**
	 * Return true if an object is hidden. This uses the "russian doll"
	 * technique to unwrap itself to the most efficient function after the first
	 * pass. This avoids repeated feature detection that would always fall into
	 * the same block of code.
	 */
	function isHidden(o) {
		if (window.getComputedStyle) {
			var cs = window.getComputedStyle;
			return (isHidden = function(o) {
				return 'none' == cs(o, null).getPropertyValue('display');
			})(o);
		} else if (window.currentStyle) {
			return (isHidden = function(o) {
				return 'none' == o.currentStyle['display'];
			})(o);
		}
		return (isHidden = function(o) {
			return 'none' == o.style['display'];
		})(o);
	}
	;

	/**
	 * Get a parent element by tag name, or the original element if it is of the
	 * tag type
	 */
	function getParent(o, a, b) {
		if (o != null && o.nodeName) {
			if (o.nodeName == a || (b && o.nodeName == b)) {
				return o;
			}
			while (o = o.parentNode) {
				if (o.nodeName && (o.nodeName == a || (b && o.nodeName == b))) {
					return o;
				}
			}
		}
		return null;
	}
	;

	/**
	 * Utility function to copy properties from one object to another
	 */
	function copy(o1, o2) {
		for ( var i = 2; i < arguments.length; i++) {
			var a = arguments[i];
			if (def(o1[a])) {
				o2[a] = o1[a];
			}
		}
	}

	// The table object itself
	var table = {
		// Class names used in the code
		AutoStripeClassName : "table-autostripe",
		StripeClassNamePrefix : "table-stripeclass:",

		AutoSortClassName : "table-autosort",
		AutoSortColumnPrefix : "table-autosort:",
		AutoSortTitle : "Click to sort",
		SortedAscendingClassName : "table-sorted-asc",
		SortedDescendingClassName : "table-sorted-desc",
		SortableClassName : "table-sortable",
		SortableColumnPrefix : "table-sortable:",
		NoSortClassName : "table-nosort",

		AutoFilterClassName : "table-autofilter",
		FilteredClassName : "table-filtered",
		FilterableClassName : "table-filterable",
		FilteredRowcountPrefix : "table-filtered-rowcount:",
		RowcountPrefix : "table-rowcount:",
		FilterAllLabel : "Filter: All",

		AutoPageSizePrefix : "table-autopage:",
		AutoPageJumpPrefix : "table-page:",
		PageNumberPrefix : "table-page-number:",
		PageCountPrefix : "table-page-count:"
	};

	/**
	 * A place to store misc table information, rather than in the table objects
	 * themselves
	 */
	table.tabledata = {};

	/**
	 * Resolve a table given an element reference, and make sure it has a unique
	 * ID
	 */
	table.uniqueId = 1;
	table.resolve = function(o, args) {
		if (o != null && o.nodeName && o.nodeName != "TABLE") {
			o = getParent(o, "TABLE");
		}
		if (o == null) {
			return null;
		}
		if (!o.id) {
			var id = null;
			do {
				var id = "TABLE_" + (table.uniqueId++);
			} while (document.getElementById(id) != null);
			o.id = id;
		}
		this.tabledata[o.id] = this.tabledata[o.id] || {};
		if (args) {
			copy(args, this.tabledata[o.id], "stripeclass", "ignorehiddenrows",
					"useinnertext", "sorttype", "col", "desc", "page",
					"pagesize");
		}
		return o;
	};

	/**
	 * Run a function against each cell in a table header or footer, usually to
	 * add or remove css classes based on sorting, filtering, etc.
	 */
	table.processTableCells = function(t, type, func, arg) {
		t = this.resolve(t);
		if (t == null) {
			return;
		}
		if (type != "TFOOT") {
			this.processCells(t.tHead, func, arg);
		}
		if (type != "THEAD") {
			this.processCells(t.tFoot, func, arg);
		}
	};

	/**
	 * Internal method used to process an arbitrary collection of cells.
	 * Referenced by processTableCells. It's done this way to avoid
	 * getElementsByTagName() which would also return nested table cells.
	 */
	table.processCells = function(section, func, arg) {
		if (section != null) {
			if (section.rows && section.rows.length && section.rows.length > 0) {
				var rows = section.rows;
				for ( var j = 0, L2 = rows.length; j < L2; j++) {
					var row = rows[j];
					if (row.cells && row.cells.length && row.cells.length > 0) {
						var cells = row.cells;
						for ( var k = 0, L3 = cells.length; k < L3; k++) {
							var cellsK = cells[k];
							func.call(this, cellsK, arg);
						}
					}
				}
			}
		}
	};

	/**
	 * Get the cellIndex value for a cell. This is only needed because of a
	 * Safari bug that causes cellIndex to exist but always be 0. Rather than
	 * feature-detecting each time it is called, the function will re-write
	 * itself the first time it is called.
	 */
	table.getCellIndex = function(td) {
		var tr = td.parentNode;
		var cells = tr.cells;
		if (cells && cells.length) {
			if (cells.length > 1 && cells[cells.length - 1].cellIndex > 0) {
				// Define the new function, overwrite the one we're running now,
				// and then run the new one
				(this.getCellIndex = function(td) {
					return td.cellIndex;
				})(td);
			}
			// Safari will always go through this slower block every time. Oh
			// well.
			for ( var i = 0, L = cells.length; i < L; i++) {
				if (tr.cells[i] == td) {
					return i;
				}
			}
		}
		return 0;
	};

	/**
	 * A map of node names and how to convert them into their "value" for
	 * sorting, filtering, etc. These are put here so it is extensible.
	 */
	table.nodeValue = {
		'INPUT' : function(node) {
			if (def(node.value)
					&& node.type
					&& ((node.type != "checkbox" && node.type != "radio") || node.checked)) {
				return node.value;
			}
			return "";
		},
		'SELECT' : function(node) {
			if (node.selectedIndex >= 0 && node.options) {
				// Sort select elements by the visible text
				return node.options[node.selectedIndex].text;
			}
			return "";
		},
		'IMG' : function(node) {
			return node.name || "";
		}
	};

	/**
	 * Get the text value of a cell. Only use innerText if explicitly told to,
	 * because otherwise we want to be able to handle sorting on inputs and
	 * other types
	 */
	table.getCellValue = function(td, useInnerText) {
		if (useInnerText && def(td.innerText)) {
			return td.innerText;
		}
		if (!td.childNodes) {
			return "";
		}
		var childNodes = td.childNodes;
		var ret = "";
		for ( var i = 0, L = childNodes.length; i < L; i++) {
			var node = childNodes[i];
			var type = node.nodeType;
			// In order to get realistic sort results, we need to treat some
			// elements in a special way.
			// These behaviors are defined in the nodeValue() object, keyed by
			// node name
			if (type == 1) {
				var nname = node.nodeName;
				if (this.nodeValue[nname]) {
					ret += this.nodeValue[nname](node);
				} else {
					ret += this.getCellValue(node);
				}
			} else if (type == 3) {
				if (def(node.innerText)) {
					ret += node.innerText;
				} else if (def(node.nodeValue)) {
					ret += node.nodeValue;
				}
			}
		}
		return ret;
	};

	/**
	 * Consider colspan and rowspan values in table header cells to calculate
	 * the actual cellIndex of a given cell. This is necessary because if the
	 * first cell in row 0 has a rowspan of 2, then the first cell in row 1 will
	 * have a cellIndex of 0 rather than 1, even though it really starts in the
	 * second column rather than the first. See:
	 * http://www.javascripttoolbox.com/temp/table_cellindex.html
	 */
	table.tableHeaderIndexes = {};
	table.getActualCellIndex = function(tableCellObj) {
		if (!def(tableCellObj.cellIndex)) {
			return null;
		}
		var tableObj = getParent(tableCellObj, "TABLE");
		var cellCoordinates = tableCellObj.parentNode.rowIndex + "-"
				+ this.getCellIndex(tableCellObj);

		// If it has already been computed, return the answer from the lookup
		// table
		if (def(this.tableHeaderIndexes[tableObj.id])) {
			return this.tableHeaderIndexes[tableObj.id][cellCoordinates];
		}

		var matrix = [];
		this.tableHeaderIndexes[tableObj.id] = {};
		var thead = getParent(tableCellObj, "THEAD");
		var trs = thead.getElementsByTagName('TR');

		// Loop thru every tr and every cell in the tr, building up a 2-d array
		// "grid" that gets
		// populated with an "x" for each space that a cell takes up. If the
		// first cell is colspan
		// 2, it will fill in values [0] and [1] in the first array, so that the
		// second cell will
		// find the first empty cell in the first row (which will be [2]) and
		// know that this is
		// where it sits, rather than its internal .cellIndex value of [1].
		for ( var i = 0; i < trs.length; i++) {
			var cells = trs[i].cells;
			for ( var j = 0; j < cells.length; j++) {
				var c = cells[j];
				var rowIndex = c.parentNode.rowIndex;
				var cellId = rowIndex + "-" + this.getCellIndex(c);
				var rowSpan = c.rowSpan || 1;
				var colSpan = c.colSpan || 1;
				var firstAvailCol;
				if (!def(matrix[rowIndex])) {
					matrix[rowIndex] = [];
				}
				var m = matrix[rowIndex];
				// Find first available column in the first row
				for ( var k = 0; k < m.length + 1; k++) {
					if (!def(m[k])) {
						firstAvailCol = k;
						break;
					}
				}
				this.tableHeaderIndexes[tableObj.id][cellId] = firstAvailCol;
				for ( var k = rowIndex; k < rowIndex + rowSpan; k++) {
					if (!def(matrix[k])) {
						matrix[k] = [];
					}
					var matrixrow = matrix[k];
					for ( var l = firstAvailCol; l < firstAvailCol + colSpan; l++) {
						matrixrow[l] = "x";
					}
				}
			}
		}
		// Store the map so future lookups are fast.
		return this.tableHeaderIndexes[tableObj.id][cellCoordinates];
	};

	/**
	 * Sort all rows in each TBODY (tbodies are sorted independent of each
	 * other)
	 */
	table.sort = function(o, args) {
		var t, tdata, sortconvert = null;
		// Allow for a simple passing of sort type as second parameter
		if (typeof (args) == "function") {
			args = {
				sorttype : args
			};
		}
		args = args || {};

		// If no col is specified, deduce it from the object sent in
		if (!def(args.col)) {
			args.col = this.getActualCellIndex(o) || 0;
		}
		// If no sort type is specified, default to the default sort
		args.sorttype = args.sorttype || Sort['default'];

		// Resolve the table
		t = this.resolve(o, args);
		tdata = this.tabledata[t.id];

		// If we are sorting on the same column as last time, flip the sort
		// direction
		if (def(tdata.lastcol) && tdata.lastcol == tdata.col
				&& def(tdata.lastdesc)) {
			tdata.desc = !tdata.lastdesc;
		} else {
			tdata.desc = !!args.desc;
		}

		// Store the last sorted column so clicking again will reverse the sort
		// order
		tdata.lastcol = tdata.col;
		tdata.lastdesc = !!tdata.desc;

		// If a sort conversion function exists, pre-convert cell values and
		// then use a plain alphanumeric sort
		var sorttype = tdata.sorttype;
		if (typeof (sorttype.convert) == "function") {
			sortconvert = tdata.sorttype.convert;
			sorttype = Sort.alphanumeric;
		}

		// Loop through all THEADs and remove sorted class names, then re-add
		// them for the col
		// that is being sorted
		this.processTableCells(t, "THEAD", function(cell) {
			if (hasClass(cell, this.SortableClassName)) {
				removeClass(cell, this.SortedAscendingClassName);
				removeClass(cell, this.SortedDescendingClassName);
				// If the computed colIndex of the cell equals the sorted
				// colIndex, flag it as sorted
				if (tdata.col == table.getActualCellIndex(cell)
						&& (classValue(cell, table.SortableClassName))) {
					addClass(cell, tdata.desc ? this.SortedAscendingClassName
							: this.SortedDescendingClassName);
				}
			}
		});

		// Sort each tbody independently
		var bodies = t.tBodies;
		if (bodies == null || bodies.length == 0) {
			return;
		}

		// Define a new sort function to be called to consider descending or not
		var newSortFunc = (tdata.desc) ? function(a, b) {
			return sorttype(b[0], a[0]);
		} : function(a, b) {
			return sorttype(a[0], b[0]);
		};

		var useinnertext = !!tdata.useinnertext;
		var col = tdata.col;

		for ( var i = 0, L = bodies.length; i < L; i++) {
			var tb = bodies[i], tbrows = tb.rows, rows = [];

			// Allow tbodies to request that they not be sorted
			if (!hasClass(tb, table.NoSortClassName)) {
				// Create a separate array which will store the converted values
				// and refs to the
				// actual rows. This is the array that will be sorted.
				var cRow, cRowIndex = 0;
				if (cRow = tbrows[cRowIndex]) {
					// Funky loop style because it's considerably faster in IE
					do {
						if (rowCells = cRow.cells) {
							var cellValue = (col < rowCells.length) ? this
									.getCellValue(rowCells[col], useinnertext)
									: null;
							if (sortconvert)
								cellValue = sortconvert(cellValue);
							rows[cRowIndex] = [ cellValue, tbrows[cRowIndex] ];
						}
					} while (cRow = tbrows[++cRowIndex])
				}

				// Do the actual sorting
				rows.sort(newSortFunc);

				// Move the rows to the correctly sorted order. Appending an
				// existing DOM object just moves it!
				cRowIndex = 0;
				var displayedCount = 0;
				var f = [ removeClass, addClass ];
				if (cRow = rows[cRowIndex]) {
					do {
						tb.appendChild(cRow[1]);
					} while (cRow = rows[++cRowIndex])
				}
			}
		}

		// If paging is enabled on the table, then we need to re-page because
		// the order of rows has changed!
		if (tdata.pagesize) {
			this.page(t); // This will internally do the striping
		} else {
			// Re-stripe if a class name was supplied
			if (tdata.stripeclass) {
				this.stripe(t, tdata.stripeclass, !!tdata.ignorehiddenrows);
			}
		}
	};

	/**
	 * Apply a filter to rows in a table and hide those that do not match.
	 */
	table.filter = function(o, filters, args) {
		var cell;
		args = args || {};

		var t = this.resolve(o, args);
		var tdata = this.tabledata[t.id];

		// If new filters were passed in, apply them to the table's list of
		// filters
		if (!filters) {
			// If a null or blank value was sent in for 'filters' then that
			// means reset the table to no filters
			tdata.filters = null;
		} else {
			// Allow for passing a select list in as the filter, since this is
			// common design
			if (filters.nodeName == "SELECT" && filters.type == "select-one"
					&& filters.selectedIndex > -1) {
				filters = {
					'filter' : filters.options[filters.selectedIndex].value
				};
			}
			// Also allow for a regular input
			if (filters.nodeName == "INPUT" && filters.type == "text") {
				filters = {
					'filter' : "/^" + filters.value + "/"
				};
			}
			// Force filters to be an array
			if (typeof (filters) == "object" && !filters.length) {
				filters = [ filters ];
			}

			// Convert regular expression strings to RegExp objects and function
			// strings to function objects
			for ( var i = 0, L = filters.length; i < L; i++) {
				var filter = filters[i];
				if (typeof (filter.filter) == "string") {
					// If a filter string is like "/expr/" then turn it into a
					// Regex
					if (filter.filter.match(/^\/(.*)\/$/)) {
						filter.filter = new RegExp(RegExp.$1);
						filter.filter.regex = true;
					}
					// If filter string is like "function (x) { ... }" then turn
					// it into a function
					else if (filter.filter
							.match(/^function\s*\(([^\)]*)\)\s*\{(.*)}\s*$/)) {
						filter.filter = Function(RegExp.$1, RegExp.$2);
					}
				}
				// If some non-table object was passed in rather than a 'col'
				// value, resolve it
				// and assign it's column index to the filter if it doesn't have
				// one. This way,
				// passing in a cell reference or a select object etc instead of
				// a table object
				// will automatically set the correct column to filter.
				if (filter && !def(filter.col)
						&& (cell = getParent(o, "TD", "TH"))) {
					filter.col = this.getCellIndex(cell);
				}

				// Apply the passed-in filters to the existing list of filters
				// for the table, removing those that have a filter of null or
				// ""
				if ((!filter || !filter.filter) && tdata.filters) {
					delete tdata.filters[filter.col];
				} else {
					tdata.filters = tdata.filters || {};
					tdata.filters[filter.col] = filter.filter;
				}
			}
			// If no more filters are left, then make sure to empty out the
			// filters object
			for ( var j in tdata.filters) {
				var keep = true;
			}
			if (!keep) {
				tdata.filters = null;
			}
		}
		// Everything's been setup, so now scrape the table rows
		return table.scrape(o);
	};

	/**
	 * "Page" a table by showing only a subset of the rows
	 */
	table.page = function(t, page, args) {
		args = args || {};
		if (def(page)) {
			args.page = page;
		}
		return table.scrape(t, args);
	};

	/**
	 * Jump forward or back any number of pages
	 */
	table.pageJump = function(t, count, args) {
		t = this.resolve(t, args);
		return this.page(t, (table.tabledata[t.id].page || 0) + count, args);
	};

	/**
	 * Go to the next page of a paged table
	 */
	table.pageNext = function(t, args) {
		return this.pageJump(t, 1, args);
	};

	table.pageFirst = function(t, agrs) {
		return this.page(t, 1, args);
	};

	/**
	 * Go to the previous page of a paged table
	 */
	table.pagePrevious = function(t, args) {
		return this.pageJump(t, -1, args);
	};

	/**
	 * Scrape a table to either hide or show each row based on filters and
	 * paging
	 */
	table.scrape = function(o, args) {
		var col, cell, filterList, filterReset = false, filter;
		var page, pagesize, pagestart, pageend;
		var unfilteredrows = [], unfilteredrowcount = 0, totalrows = 0;
		var t, tdata, row, hideRow;
		args = args || {};

		// Resolve the table object
		t = this.resolve(o, args);
		tdata = this.tabledata[t.id];

		// Setup for Paging
		var page = tdata.page;
		if (def(page)) {
			// Don't let the page go before the beginning
			if (page < 0) {
				tdata.page = page = 0;
			}
			pagesize = tdata.pagesize || 25; // 25=arbitrary default
			pagestart = page * pagesize + 1;
			pageend = pagestart + pagesize - 1;
		}

		// Scrape each row of each tbody
		var bodies = t.tBodies;
		if (bodies == null || bodies.length == 0) {
			return;
		}
		for ( var i = 0, L = bodies.length; i < L; i++) {
			var tb = bodies[i];
			for ( var j = 0, L2 = tb.rows.length; j < L2; j++) {
				row = tb.rows[j];
				hideRow = false;

				// Test if filters will hide the row
				if (tdata.filters && row.cells) {
					var cells = row.cells;
					var cellsLength = cells.length;
					// Test each filter
					for (col in tdata.filters) {
						if (!hideRow) {
							filter = tdata.filters[col];
							if (filter && col < cellsLength) {
								var val = this.getCellValue(cells[col]);
								if (filter.regex && val.search) {
									hideRow = (val.search(filter) < 0);
								} else if (typeof (filter) == "function") {
									hideRow = !filter(val, cells[col]);
								} else {
									hideRow = (val != filter);
								}
							}
						}
					}
				}

				// Keep track of the total rows scanned and the total runs _not_
				// filtered out
				totalrows++;
				if (!hideRow) {
					unfilteredrowcount++;
					if (def(page)) {
						// Temporarily keep an array of unfiltered rows in case
						// the page we're on goes past
						// the last page and we need to back up. Don't want to
						// filter again!
						unfilteredrows.push(row);
						if (unfilteredrowcount < pagestart
								|| unfilteredrowcount > pageend) {
							hideRow = true;
						}
					}
				}

				row.style.display = hideRow ? "none" : "";
			}
		}

		if (def(page)) {
			// Check to see if filtering has put us past the requested page
			// index. If it has,
			// then go back to the last page and show it.
			if (pagestart >= unfilteredrowcount) {
				pagestart = unfilteredrowcount
						- (unfilteredrowcount % pagesize);
				tdata.page = page = pagestart / pagesize;
				for ( var i = pagestart, L = unfilteredrows.length; i < L; i++) {
					unfilteredrows[i].style.display = "";
				}
			}
		}

		// Loop through all THEADs and add/remove filtered class names
		this
				.processTableCells(
						t,
						"THEAD",
						function(c) {
							((tdata.filters
									&& def(tdata.filters[table.getCellIndex(c)]) && hasClass(
									c, table.FilterableClassName)) ? addClass
									: removeClass)(c, table.FilteredClassName);
						});

		// Stripe the table if necessary
		if (tdata.stripeclass) {
			this.stripe(t);
		}

		// Calculate some values to be returned for info and updating purposes
		var pagecount = Math.floor(unfilteredrowcount / pagesize) + 1;
		if (def(page)) {
			// Update the page number/total containers if they exist
			if (tdata.container_number) {
				tdata.container_number.innerHTML = page + 1;
			}
			if (tdata.container_count) {
				tdata.container_count.innerHTML = pagecount;
			}
		}

		// Update the row count containers if they exist
		if (tdata.container_filtered_count) {
			tdata.container_filtered_count.innerHTML = unfilteredrowcount;
		}
		if (tdata.container_all_count) {
			tdata.container_all_count.innerHTML = totalrows;
		}
		return {
			'data' : tdata,
			'unfilteredcount' : unfilteredrowcount,
			'total' : totalrows,
			'pagecount' : pagecount,
			'page' : page,
			'pagesize' : pagesize
		};
	};

	/**
	 * Shade alternate rows, aka Stripe the table.
	 */
	table.stripe = function(t, className, args) {
		args = args || {};
		args.stripeclass = className;

		t = this.resolve(t, args);
		var tdata = this.tabledata[t.id];

		var bodies = t.tBodies;
		if (bodies == null || bodies.length == 0) {
			return;
		}

		className = tdata.stripeclass;
		// Cache a shorter, quicker reference to either the remove or add class
		// methods
		var f = [ removeClass, addClass ];
		for ( var i = 0, L = bodies.length; i < L; i++) {
			var tb = bodies[i], tbrows = tb.rows, cRowIndex = 0, cRow, displayedCount = 0;
			if (cRow = tbrows[cRowIndex]) {
				// The ignorehiddenrows test is pulled out of the loop for a
				// slight speed increase.
				// Makes a bigger difference in FF than in IE.
				// In this case, speed always wins over brevity!
				if (tdata.ignoreHiddenRows) {
					do {
						f[displayedCount++ % 2](cRow, className);
					} while (cRow = tbrows[++cRowIndex])
				} else {
					do {
						if (!isHidden(cRow)) {
							f[displayedCount++ % 2](cRow, className);
						}
					} while (cRow = tbrows[++cRowIndex])
				}
			}
		}
	};

	/**
	 * Build up a list of unique values in a table column
	 */
	table.getUniqueColValues = function(t, col) {
		var values = {}, bodies = this.resolve(t).tBodies;
		for ( var i = 0, L = bodies.length; i < L; i++) {
			var tbody = bodies[i];
			for ( var r = 0, L2 = tbody.rows.length; r < L2; r++) {
				values[this.getCellValue(tbody.rows[r].cells[col])] = true;
			}
		}
		var valArray = [];
		for ( var val in values) {
			valArray.push(val);
		}
		return valArray.sort();
	};

	/**
	 * Scan the document on load and add sorting, filtering, paging etc ability
	 * automatically based on existence of class names on the table and cells.
	 */
	table.auto = function(args) {
		var cells = [], tables = document.getElementsByTagName("TABLE");
		var val, tdata;
		if (tables != null) {
			for ( var i = 0, L = tables.length; i < L; i++) {
				var t = table.resolve(tables[i]);
				tdata = table.tabledata[t.id];
				if (val = classValue(t, table.StripeClassNamePrefix)) {
					tdata.stripeclass = val;
				}
				// Do auto-filter if necessary
				if (hasClass(t, table.AutoFilterClassName)) {
					table.autofilter(t);
				}
				// Do auto-page if necessary
				if (val = classValue(t, table.AutoPageSizePrefix)) {
					table.autopage(t, {
						'pagesize' : +val
					});
				}
				// Do auto-sort if necessary
				if ((val = classValue(t, table.AutoSortColumnPrefix))
						|| (hasClass(t, table.AutoSortClassName))) {
					table.autosort(t, {
						'col' : (val == null) ? null : +val
					});
				}
				// Do auto-stripe if necessary
				if (tdata.stripeclass && hasClass(t, table.AutoStripeClassName)) {
					table.stripe(t);
				}
			}
		}
	};

	/**
	 * Add sorting functionality to a table header cell
	 */
	table.autosort = function(t, args) {
		t = this.resolve(t, args);
		var tdata = this.tabledata[t.id];
		this.processTableCells(t, "THEAD", function(c) {
			var type = classValue(c, table.SortableColumnPrefix);
			if (type != null) {
				type = type || "default";
				c.title = c.title || table.AutoSortTitle;
				addClass(c, table.SortableClassName);
				c.onclick = Function("", "Table.sort(this,{'sorttype':Sort['"
						+ type + "']})");
				// If we are going to auto sort on a column, we need to keep
				// track of what kind of sort it will be
				if (args.col != null) {
					if (args.col == table.getActualCellIndex(c)) {
						tdata.sorttype = Sort['"+type+"'];
					}
				}
			}
		});
		if (args.col != null) {
			table.sort(t, args);
		}
	};

	/**
	 * Add paging functionality to a table
	 */
	table.autopage = function(t, args) {
		t = this.resolve(t, args);
		var tdata = this.tabledata[t.id];
		if (tdata.pagesize) {
			this.processTableCells(t, "THEAD,TFOOT", function(c) {
				var type = classValue(c, table.AutoPageJumpPrefix);
				if (type == "next") {
					type = 1;
					c.onclick = Function("", "Table.pageJump(this," + type
							+ ")");
				} else if (type == "previous") {
					type = -1;
					c.onclick = Function("", "Table.pageJump(this," + type
							+ ")");
				} else if (type == "first") {
					c.onclick = Function("", "Table.page(this,0)");
				} else if (type == "last") {
					c.onclick = Function("", "Table.page(this,10000)");
				}
			});
			if (val = classValue(t, table.PageNumberPrefix)) {
				tdata.container_number = document.getElementById(val);
			}
			if (val = classValue(t, table.PageCountPrefix)) {
				tdata.container_count = document.getElementById(val);
			}
			return table.page(t, 0, args);
		}
	};

	/**
	 * A util function to cancel bubbling of clicks on filter dropdowns
	 */
	table.cancelBubble = function(e) {
		e = e || window.event;
		if (typeof (e.stopPropagation) == "function") {
			e.stopPropagation();
		}
		if (def(e.cancelBubble)) {
			e.cancelBubble = true;
		}
	};

	/**
	 * Auto-filter a table
	 */
	table.autofilter = function(t, args) {
		args = args || {};
		t = this.resolve(t, args);
		var tdata = this.tabledata[t.id], val;
		table
				.processTableCells(
						t,
						"THEAD",
						function(cell) {
							if (hasClass(cell, table.FilterableClassName)) {
								var cellIndex = table.getCellIndex(cell);
								var colValues = table.getUniqueColValues(t,
										cellIndex);
								if (colValues.length > 0) {
									if (typeof (args.insert) == "function") {
										func.insert(cell, colValues);
									} else {
										var sel = '<select onchange="Table.filter(this,this)" onclick="Table.cancelBubble(event)" class="'
												+ table.AutoFilterClassName
												+ '"><option value="">'
												+ table.FilterAllLabel
												+ '</option>';
										for ( var i = 0; i < colValues.length; i++) {
											sel += '<option value="'
													+ colValues[i] + '">'
													+ colValues[i]
													+ '</option>';
										}
										sel += '</select>';
										cell.innerHTML += "<br>" + sel;
									}
								}
							}
						});
		if (val = classValue(t, table.FilteredRowcountPrefix)) {
			tdata.container_filtered_count = document.getElementById(val);
		}
		if (val = classValue(t, table.RowcountPrefix)) {
			tdata.container_all_count = document.getElementById(val);
		}
	};

	/**
	 * Attach the auto event so it happens on load. use jQuery's ready()
	 * function if available
	 */
	if (typeof (jQuery) != "undefined") {
		jQuery(table.auto);
	} else if (window.addEventListener) {
		window.addEventListener("load", table.auto, false);
	} else if (window.attachEvent) {
		window.attachEvent("onload", table.auto);
	}

	return table;
})();